import utils from '../utils'
import Common from '../common.js'
import ShpConverterTopology from './index.js'
import {ArcIndex} from './index.js'
// Converts all polygon and polyline paths in a dataset to a topological format,
// (in-place);
function buildTopology(dataset) {
  if (!dataset.arcs) return
  const raw = dataset.arcs.getVertexData()
  const cooked = buildPathTopology(raw.nn, raw.xx, raw.yy)
  dataset.arcs.updateVertexData(cooked.nn, cooked.xx, cooked.yy)
  dataset.layers.forEach((lyr) => {
    if (lyr.geometry_type == 'polyline' || lyr.geometry_type == 'polygon') {
      lyr.shapes = replaceArcIds(lyr.shapes, cooked.paths)
    }
  })
}
function buildPathTopology(nn, xx, yy) {
  const pointCount = xx.length
  const chainIds = ShpConverterTopology.initPointChains(xx, yy)
  const pathIds = initPathIds(pointCount, nn)
  const index = new ArcIndex(pointCount)
  const slice = usingTypedArrays() ? xx.subarray : Array.prototype.slice
  const paths = convertPaths(nn)
  const retn = index.getVertexData()
  retn.paths = paths
  return retn

  function usingTypedArrays() {
    return !!(xx.subarray && yy.subarray)
  }

  function convertPaths(nn) {
    const paths = []
    let pointId = 0
    for (let i=0, len=nn.length; i<len; i++) {
      const pathLen = nn[i]
      paths.push(pathLen < 2 ? null : convertPath(pointId, pointId + pathLen - 1))
      pointId += pathLen
    }
    return paths
  }

  function nextPoint(id) {
    const partId = pathIds[id]
    const nextId = id + 1
    if (nextId < pointCount && pathIds[nextId] === partId) {
      return id + 1
    }
    const len = nn[partId]
    return sameXY(id, id - len + 1) ? id - len + 2 : -1
  }

  function prevPoint(id) {
    const partId = pathIds[id]
    const prevId = id - 1
    if (prevId >= 0 && pathIds[prevId] === partId) {
      return id - 1
    }
    const len = nn[partId]
    return sameXY(id, id + len - 1) ? id + len - 2 : -1
  }

  function sameXY(a, b) {
    return xx[a] == xx[b] && yy[a] == yy[b]
  }

  // Convert a non-topological path to one or more topological arcs
  // @start, @end are ids of first and last points in the path
  // TODO: don't allow id ~id pairs
  //
  function convertPath(start, end) {
    const arcIds = []
    let firstNodeId = -1
    let arcStartId = null

    // Visit each point in the path, up to but not including the last point
    let i = 0
    for (i = start; i < end; i++) {
      if (pointIsArcEndpoint(i)) {
        if (firstNodeId > -1) {
          arcIds.push(addEdge(arcStartId, i))
        } else {
          firstNodeId = i
        }
        arcStartId = i
      }
    }

    // Identify the final arc in the path
    if (firstNodeId == -1) {
      // Not in an arc, i.e. no nodes have been found...
      // Assuming that path is either an island or is congruent with one or more rings
      arcIds.push(addRing(start, end))
    }
    else if (firstNodeId == start) {
      // path endpoint is a node;
      if (!pointIsArcEndpoint(end)) {
        utils.error("Topology error") // TODO: better error handling
      }
      arcIds.push(addEdge(arcStartId, i))
    } else {
      // final arc wraps around
      arcIds.push(addSplitEdge(arcStartId, end, start + 1, firstNodeId))
    }
    return arcIds
  }

  // Test if a point @id is an endpoint of a topological path
  function pointIsArcEndpoint(id) {
    let id2 = chainIds[id]
    const prev = prevPoint(id)
    const next = nextPoint(id)
    if (prev == -1 || next == -1) {
      // @id is an endpoint if it is the start or end of an open path
      return true
    }
    while (id != id2) {
      const prev2 = prevPoint(id2);
      const next2 = nextPoint(id2);
      if (prev2 == -1 || next2 == -1 || brokenEdge(prev, next, prev2, next2)) {
        // there is a discontinuity at @id -- point is arc endpoint
        return true
      }
      id2 = chainIds[id2]
    }
    return false
  }

  // a and b are two vertices with the same x, y coordinates
  // test if the segments on either side of them are also identical
  function brokenEdge(aprev, anext, bprev, bnext) {
    const apx = xx[aprev]
    const anx = xx[anext]
    const bpx = xx[bprev]
    const bnx = xx[bnext]
    const apy = yy[aprev]
    const any = yy[anext]
    const bpy = yy[bprev]
    const bny = yy[bnext]
    if (apx == bnx && anx == bpx && apy == bny && any == bpy ||
        apx == bpx && anx == bnx && apy == bpy && any == bny) {
      return false
    }
    return true
  }

  function mergeArcParts(src, startId, endId, startId2, endId2) {
    const len = endId - startId + endId2 - startId2 + 2
    const ArrayClass = usingTypedArrays() ? Float64Array : Array
    const dest = new ArrayClass(len)
    let j = 0
    let i = null
    for (i=startId; i <= endId; i++) {
      dest[j++] = src[i]
    }
    for (i=startId2; i <= endId2; i++) {
      dest[j++] = src[i]
    }
    return dest
  }

  function addSplitEdge(start1, end1, start2, end2) {
    let arcId = index.findDuplicateArc(xx, yy, start1, end2, nextPoint, prevPoint)
    if (arcId === null) {
      arcId = index.addArc(mergeArcParts(xx, start1, end1, start2, end2),
          mergeArcParts(yy, start1, end1, start2, end2))
    }
    return arcId
  }

  function addEdge(start, end) {
    // search for a matching edge that has already been generated
    let arcId = index.findDuplicateArc(xx, yy, start, end, nextPoint, prevPoint)
    if (arcId === null) {
      arcId = index.addArc(slice.call(xx, start, end + 1),
          slice.call(yy, start, end + 1))
    }
    return arcId
  }

  function addRing(startId, endId) {
    let chainId = chainIds[startId]
    const pathId = pathIds[startId]

    while (chainId != startId) {
      if (pathIds[chainId] < pathId) {
        break
      }
      chainId = chainIds[chainId]
    }

    if (chainId == startId) {
      return addEdge(startId, endId)
    }

    for (let i=startId; i<endId; i++) {
      const arcId = index.findDuplicateArc(xx, yy, i, i, nextPoint, prevPoint)
      if (arcId !== null) return arcId
    }
    // error("Unmatched ring; id:", pathId, "len:", nn[pathId])
    utils.error(`Unmatched ring; id:${pathId},len:${nn[pathId]}`)
  }
}
// Create a lookup table for path ids; path ids are indexed by point id
//
function initPathIds(size, pathSizes) {
  const pathIds = new Int32Array(size)
  let j = 0
  for (let pathId=0, pathCount=pathSizes.length; pathId < pathCount; pathId++) {
    for (let i=0, n=pathSizes[pathId]; i<n; i++, j++) {
      pathIds[j] = pathId
    }
  }
  return pathIds
}
function replaceArcIds(src, replacements) {
  return src.map(function(shape) {
    return replaceArcsInShape(shape, replacements)
  })

  function replaceArcsInShape(shape, replacements) {
    if (!shape) return null
    return shape.map(function(path) {
      return replaceArcsInPath(path, replacements)
    })
  }

  function replaceArcsInPath(path, replacements) {
    return path.reduce((memo, id) => {
      const abs = Common.absArcId(id)
      let topoPath = replacements[abs]
      if (topoPath) {
        if (id < 0) {
          topoPath = topoPath.concat() // TODO: need to copy?
          utils.reversePath(topoPath)
        }
        for (let i=0, n=topoPath.length; i<n; i++) {
          memo.push(topoPath[i])
        }
      }
      return memo
    }, [])
  }
}
export default buildTopology